#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include "iec_types.h"

#define __INIT_STRING (IEC_STRING){0,""}

static inline void __normalize_timespec(IEC_TIMESPEC *ts) {
    if (ts->tv_nsec < -1000000000 || ((ts->tv_sec > 0) && (ts->tv_nsec < 0))) {
        ts->tv_sec--;
        ts->tv_nsec += 1000000000;
    }
    if (ts->tv_nsec > 1000000000 || ((ts->tv_sec < 0) && (ts->tv_nsec > 0))) {
        ts->tv_sec++;
        ts->tv_nsec -= 1000000000;
    }
}

IEC_TIME iec_time_mul(IEC_TIME IN1, IEC_LREAL IN2) {
    IEC_LREAL s_f = IN1.tv_sec * IN2;
    time_t s = (time_t) s_f;
    div_t ns = div((int) ((IEC_LREAL) IN1.tv_nsec * IN2), 1000000000);
    IEC_TIME res = {(long) s + ns.quot,
                    (long) ns.rem + (s_f - s) * 1000000000};
    __normalize_timespec(&res);
    return res;
}

IEC_TIME iec_time_div(IEC_TIME IN1, IEC_LREAL IN2) {
    IEC_LREAL s_f = IN1.tv_sec / IN2;
    time_t s = (time_t) s_f;
    IEC_TIME res = {(long) s,
                    (long) (IN1.tv_nsec / IN2 + (s_f - s) * 1000000000)};
    __normalize_timespec(&res);
    return res;
}

IEC_STRING iec_bit_to_string(IEC_LWORD IN) {
    IEC_STRING res;
    res = (IEC_STRING) {0, ""};
    res.len = snprintf((char *) res.body, STR_MAX_LEN, "16#%llx", (long long unsigned int) IN);
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

IEC_STRING iec_real_to_string(IEC_LREAL IN) {
    IEC_STRING res;
    res = (IEC_STRING) {0, ""};;
    res.len = snprintf((char *) res.body, STR_MAX_LEN, "%.10g", IN);
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

IEC_STRING iec_sint_to_string(IEC_LINT IN) {
    IEC_STRING res;
    res = (IEC_STRING) {0, ""};;
    res.len = snprintf((char *) res.body, STR_MAX_LEN, "%lld", (long long int) IN);
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

IEC_STRING iec_uint_to_string(IEC_ULINT IN) {
    IEC_STRING res;
    res = (IEC_STRING) {0, ""};
    res.len = snprintf((char *) res.body, STR_MAX_LEN, "%llu", (long long unsigned int) IN);
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

IEC_BOOL iec_string_to_bool(IEC_STRING IN) {
    int i;
    if (IN.len == 1) return !memcmp(&IN.body, "1", IN.len);
    for (i = 0; i < IN.len; i++) IN.body[i] = toupper(IN.body[i]);
    return IN.len == 4 ? !memcmp(&IN.body, "TRUE", IN.len) : 0;
}

static inline IEC_LINT __pstring_to_sint(IEC_STRING *IN) {
    IEC_LINT res = 0;
    __strlen_t l;
    unsigned int shift = 0;

    if (IN->body[0] == '2' && IN->body[1] == '#') {
        /* 2#0101_1010_1011_1111 */
        for (l = IN->len - 1; l >= 2 && shift < 64; l--) {
            char c = IN->body[l];
            if (c >= '0' && c <= '1') {
                res |= (c - '0') << shift;
                shift += 1;
            }
        }
    } else if (IN->body[0] == '8' && IN->body[1] == '#') {
        /* 8#1234_5665_4321 */
        for (l = IN->len - 1; l >= 2 && shift < 64; l--) {
            char c = IN->body[l];
            if (c >= '0' && c <= '7') {
                res |= (c - '0') << shift;
                shift += 3;
            }
        }
    } else if (IN->body[0] == '1' && IN->body[1] == '6' && IN->body[2] == '#') {
        /* 16#1234_5678_9abc_DEFG */
        for (l = IN->len - 1; l >= 3 && shift < 64; l--) {
            char c = IN->body[l];
            if (c >= '0' && c <= '9') {
                res |= (IEC_LWORD) (c - '0') << shift;
                shift += 4;
            } else if (c >= 'a' && c <= 'f') {
                res |= (IEC_LWORD) (c - 'a' + 10) << shift;
                shift += 4;
            } else if (c >= 'A' && c <= 'F') {
                res |= (IEC_LWORD) (c - 'A' + 10) << shift;
                shift += 4;
            }
        }
    } else {
        /* -123456789 */
        IEC_LINT fac = IN->body[0] == '-' ? -1 : 1;
        for (l = IN->len - 1; l >= 0 && shift < 20; l--) {
            char c = IN->body[l];
            if (c >= '0' && c <= '9') {
                res += (c - '0') * fac;
                fac *= 10;
                shift += 1;
            } else if (c >= '.') { /* reset value */
                res = 0;
                fac = IN->body[0] == '-' ? -1 : 1;
                shift = 0;
            }
        }
    }
    return res;
}

IEC_LREAL iec_string_to_real(IEC_STRING IN) {
    __strlen_t l;
    l = IN.len;
    /* search the dot */
    while (--l > 0 && IN.body[l] != '.');
    if (l != 0) {
        return atof((const char *) &IN.body);
    } else {
        return (IEC_LREAL) __pstring_to_sint(&IN);
    }
}

IEC_TIME iec_string_to_time(IEC_STRING IN) {
    __strlen_t l;
    /* TODO :
     *
     *  Duration literals without underlines: T#14ms    T#-14ms   T#14.7s   T#14.7m
     *                short prefix            T#14.7h    t#14.7d   t#25h15m
     *                                        t#5d14h12m18s3.5ms
     *                long prefix             TIME#14ms    TIME#-14ms   time#14.7s
     *  Duration literals with underlines:
     *                short prefix            t#25h_15m t#5d_14h_12m_18s_3.5ms
     *                long prefix             TIME#25h_15m
     *                                        time#5d_14h_12m_18s_3.5ms
     *
     *  Long prefix notation                 Short prefix notation
     *  DATE#1984-06-25                      D#1984-06-25
     *  date#1984-06-25                      d#1984-06-25
     *  TIME_OF_DAY#15:36:55.36              TOD#15:36:55.36
     *  time_of_day#15:36:55.36              tod#15:36:55.36
     *  DATE_AND_TIME#1984-06-25-15:36:55.36 DT#1984-06-25-15:36:55.36
     *  date_and_time#1984-06-25-15:36:55.36 dt#1984-06-25-15:36:55.36
     *
     */
    /* Quick hack : only transform seconds */
    /* search the dot */
    l = IN.len;
    while (--l > 0 && IN.body[l] != '.');
    if (l != 0) {
        IEC_LREAL IN_val = atof((const char *) &IN.body);
        return (IEC_TIME) {(long) IN_val, (long) (IN_val - (IEC_LINT) IN_val) * 1000000000};
    } else {
        return (IEC_TIME) {(long) __pstring_to_sint(&IN), 0};
    }
}

#define EPOCH_YEAR 1970
#define SECONDS_PER_MINUTE 60
#define SECONDS_PER_HOUR (60 * SECONDS_PER_MINUTE)
#define SECONDS_PER_DAY (24 * SECONDS_PER_HOUR)

IEC_STRING iec_time_to_string(IEC_TIME IN) {
    IEC_STRING res;
    div_t days;
    /*t#5d14h12m18s3.5ms*/
    res = (IEC_STRING) {0, ""};
    days = div((int) IN.tv_sec, (int) SECONDS_PER_DAY);
    if (!days.rem && IN.tv_nsec == 0) {
        res.len = snprintf((char *) &res.body, STR_MAX_LEN, "T#%dd", days.quot);
    } else {
        div_t hours = div(days.rem, SECONDS_PER_HOUR);
        if (!hours.rem && IN.tv_nsec == 0) {
            res.len = snprintf((char *) &res.body, STR_MAX_LEN, "T#%dd%dh", days.quot, hours.quot);
        } else {
            div_t minuts = div(hours.rem, SECONDS_PER_MINUTE);
            if (!minuts.rem && IN.tv_nsec == 0) {
                res.len = snprintf((char *) &res.body, STR_MAX_LEN, "T#%dd%dh%dm", days.quot, hours.quot, minuts.quot);
            } else {
                if (IN.tv_nsec == 0) {
                    res.len = snprintf((char *) &res.body, STR_MAX_LEN, "T#%dd%dh%dm%ds", days.quot, hours.quot,
                                       minuts.quot, minuts.rem);
                } else {
                    res.len = snprintf((char *) &res.body, STR_MAX_LEN, "T#%dd%dh%dm%ds%gms", days.quot, hours.quot,
                                       minuts.quot, minuts.rem, (IEC_LREAL) IN.tv_nsec / 1000000);
                }
            }
        }
    }
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

#define __isleap(year) \
  ((year) % 4 == 0 && ((year) % 100 != 0 || (year) % 400 == 0))
static const unsigned short int __mon_yday[2][13] =
        {
                /* Normal years.  */
                {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
                /* Leap years.  */
                {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}
        };

static inline struct tm convert_seconds_to_date_and_time(long int seconds) {
    struct tm dt;
    long int days, rem;
    days = seconds / SECONDS_PER_DAY;
    rem = seconds % SECONDS_PER_DAY;
    if (rem < 0) {
        rem += SECONDS_PER_DAY;
        days--;
    }

    // time of day
    dt.tm_hour = rem / SECONDS_PER_HOUR;
    rem %= SECONDS_PER_HOUR;
    dt.tm_min = rem / 60;
    dt.tm_sec = rem % 60;

    // date
    dt.tm_year = EPOCH_YEAR;
    while (days >= (rem = __isleap(dt.tm_year) ? 366 : 365)) {
        dt.tm_year++;
        days -= rem;
    }
    while (days < 0) {
        dt.tm_year--;
        days += __isleap(dt.tm_year) ? 366 : 365;
    }
    dt.tm_mon = 1;
    while (days >= __mon_yday[__isleap(dt.tm_year)][dt.tm_mon]) {
        dt.tm_mon += 1;
    }
    dt.tm_mday = days - __mon_yday[__isleap(dt.tm_year)][dt.tm_mon - 1] + 1;

    return dt;
}

IEC_STRING iec_date_to_string(IEC_DATE IN) {
    IEC_STRING res;
    struct tm broken_down_time;
    /* D#1984-06-25 */
    broken_down_time = convert_seconds_to_date_and_time(IN.tv_sec);
    res = __INIT_STRING;
    res.len = snprintf((char *) &res.body, STR_MAX_LEN, "D#%d-%2.2d-%2.2d",
                       broken_down_time.tm_year,
                       broken_down_time.tm_mon,
                       broken_down_time.tm_mday);
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

void __iec_error(void) {
    /* TODO... */
    fprintf(stderr, "IEC 61131-3 runtime error.\n");
    /*exit(1);*/
}

IEC_STRING iec_tod_to_string(IEC_TOD IN) {
    IEC_STRING res;
    struct tm broken_down_time;
    time_t seconds;
    /* TOD#15:36:55.36 */
    seconds = IN.tv_sec;
    if (seconds >= SECONDS_PER_DAY) {
        __iec_error();
        return (IEC_STRING) {9, "TOD#ERROR"};
    }
    broken_down_time = convert_seconds_to_date_and_time(seconds);
    res = __INIT_STRING;
    if (IN.tv_nsec == 0) {
        res.len = snprintf((char *) &res.body, STR_MAX_LEN, "TOD#%2.2d:%2.2d:%2.2d",
                           broken_down_time.tm_hour,
                           broken_down_time.tm_min,
                           broken_down_time.tm_sec);
    } else {
        res.len = snprintf((char *) &res.body, STR_MAX_LEN, "TOD#%2.2d:%2.2d:%09.6f",
                           broken_down_time.tm_hour,
                           broken_down_time.tm_min,
                           (IEC_LREAL) broken_down_time.tm_sec + (IEC_LREAL) IN.tv_nsec / 1e9);
    }
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}

IEC_STRING iec_dt_to_string(IEC_DT IN) {
    IEC_STRING res;
    struct tm broken_down_time;
    /* DT#1984-06-25-15:36:55.36 */
    broken_down_time = convert_seconds_to_date_and_time(IN.tv_sec);
    if (IN.tv_nsec == 0) {
        res.len = snprintf((char *) &res.body, STR_MAX_LEN, "DT#%d-%2.2d-%2.2d-%2.2d:%2.2d:%2.2d",
                           broken_down_time.tm_year,
                           broken_down_time.tm_mon,
                           broken_down_time.tm_mday,
                           broken_down_time.tm_hour,
                           broken_down_time.tm_min,
                           broken_down_time.tm_sec);
    } else {
        res.len = snprintf((char *) &res.body, STR_MAX_LEN, "DT#%d-%2.2d-%2.2d-%2.2d:%2.2d:%09.6f",
                           broken_down_time.tm_year,
                           broken_down_time.tm_mon,
                           broken_down_time.tm_mday,
                           broken_down_time.tm_hour,
                           broken_down_time.tm_min,
                           (IEC_LREAL) broken_down_time.tm_sec + ((IEC_LREAL) IN.tv_nsec / 1e9));
    }
    if (res.len > STR_MAX_LEN) res.len = STR_MAX_LEN;
    return res;
}
